package com.mspbots.web.core.service;


import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mspbots.api.RestServerException;
import com.mspbots.web.BaseService;
import com.mspbots.web.config.RabbitConfiguration;
import com.mspbots.web.core.dto.SysTenantDTO;
import com.mspbots.web.core.model.Tenant;
import com.mspbots.web.core.model.TenantUser;
import com.mspbots.web.params.TenantParam;
import com.mspbots.web.params.UserParam;
import com.mspbots.web.security.User;
import com.mspbots.web.security.service.UsersService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.query.Criteria;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static com.mspbots.web.security.service.AuthService.DEFAULT_TENANT_ADMIN_ROLE_NAME;
import static com.mspbots.web.security.service.AuthService.DEFAULT_TENANT_USER_ROLE_NAME;

/**
 * com.mspbots.web.core.service.TenantService
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/7/9
 */
@Log4j2
@Service
@RequiredArgsConstructor
public class TenantService extends BaseService {

    private final String CACHING_TENANT_KEY_PREFIX = "caching:web:tenant:";

    private final DatabaseClient databaseClient;
    private final UsersService usersService;

    public Flux<Tenant> loadAll() {
        return this.coreClient.getFlux("/tenant", null, Tenant.class)
                .flatMap(tenant -> this.coreClient.get("/tenant/user/count",
                        Map.of("tenantId", tenant.getId()), Integer.class)
                        .defaultIfEmpty(0)
                        .map(count -> {
                            tenant.setUserCount(count);
                            return tenant;
                        })
                ).flatMap(tenant -> this.usersService.loadByTenantLast(tenant.getId())
                        .defaultIfEmpty(new User())
                        .map(user -> {
                            tenant.setLastUserLogged(user.getFirstName() + " " + user.getFirstName());
                            tenant.setLastUserLoggedTime(user.getLastLogin());
                            return tenant;
                        })
                ).sort((obj1, obj2) -> obj2.getId().compareTo(obj1.getId()));
    }

    public Mono<Tenant> loadById(Integer id) {
        return this.reactiveRedisTemplate.opsForValue().get(this.CACHING_TENANT_KEY_PREFIX + id)
                .map(tenant -> this.objectMapper.convertValue(tenant, Tenant.class))
                .switchIfEmpty(this.databaseClient.select().from(Tenant.class).matching(Criteria.where("id").is(id))
                        .fetch().one().doOnNext(tenant -> this.reactiveRedisTemplate.opsForValue()
                                .set(this.CACHING_TENANT_KEY_PREFIX + tenant.getId(), tenant, Duration.ofHours(2))
                                .subscribe())
                );
    }

    public Mono<Boolean> deleteTenant(Long tenantId) {
        return this.coreClient.getFlux("/tenant/delete-tenant/" + tenantId)
                .flatMap(e -> this.reactiveRedisTemplate.keys("caching:web:user:*")
                        .flatMap(kes -> this.reactiveRedisTemplate.delete(kes)))
                .flatMap(e -> this.wiseClient.get("/callbacks/" + tenantId)
                        .flatMapMany(res -> {
                            log.debug("callbacks res::" + res.asText());
                            List<Mono<JsonNode>> resulMonos = new ArrayList<>();
                            res.elements().forEachRemaining(x -> {
                                if (x.get("url").asText().contains("mspbots")) {
                                    if ("ticket".equalsIgnoreCase(x.get("type").asText()) ||
                                            "schedule".equalsIgnoreCase(x.get("type").asText())) {
                                        resulMonos.add(this.wiseClient.delete("/callbacks/" + tenantId + "/" + x.get("id").asLong(),
                                                null).retry(3));
                                    }
                                }
                            });
                            return Flux.concat(resulMonos);
                        }))
                .collectList()
                .map(list -> list.size() > 0);

//        return this.reactiveRedisTemplate.keys("caching:web:user:*")
//                .flatMap(kes -> this.reactiveRedisTemplate.delete(kes))
//                .doOnNext(e -> this.coreClient.get("/tenant/delete-tenant/" + tenantId)
//                        .subscribe(res -> {
//                            log.info("delete-tenant res::"+res.asText());
//                        }))
//                .doOnNext(e -> this.wiseClient.get("/callbacks/" + tenantId)
//                        .subscribe(res -> {
//                            log.info("callbacks res::"+res.asText());
//                            res.elements().forEachRemaining(x -> {
//                                if (x.get("url").asText().contains("mspbots")) {
//                                    if (x.get("type").asText().equalsIgnoreCase("ticket") ||
//                                            x.get("type").asText().equalsIgnoreCase("schedule")) {
//                                        this.wiseClient.delete("/callbacks/" + tenantId + "/" + x.get("id").asLong(), null).subscribe();
//                                    }
//                                }
//                            });
//                        }))
//                .map(r -> r > 0);
    }

    public Mono<Tenant> findByTenantId(Long tenantId) {
        return this.coreClient.get("/tenant/" + tenantId + "/info", null, Tenant.class);
    }

    public Mono<Tenant> create(TenantParam entity) {
        entity.setStatusId(1);
        entity.setReceiveEmail(entity.getReceiveEmail());
        entity.setRegistEmail(entity.getMailPostfix());
        return this.coreClient.post("/tenant", entity, Tenant.class)
                .onErrorResume(e -> Mono.defer(() ->
                        Mono.error(new TenantIsExistsException(500, "This Client [" + entity.getShortName() + "] already exists. " +
                                "Please check with your administrator \"email\" to sync the active directory."))))
                .doOnNext(tenant -> this.usersService.register(UserParam.builder()
                        .username(entity.getUsername())
                        .roles(Set.of(DEFAULT_TENANT_ADMIN_ROLE_NAME, DEFAULT_TENANT_USER_ROLE_NAME))
                        .password(entity.getPassword())
                        .tenantId(tenant.getId()).email(tenant.getMailPostfix())
                        .firstName(entity.getFirstName()).lastName(entity.getLastName()).build())
                        .subscribe(result -> {
                                    log.info("Create tenant {} administrator user success", tenant.getId());
                                    SysTenantDTO sysTenantDTO = SysTenantDTO.builder()
                                            .tenantCode((long) tenant.getId())
                                            .tenantName(tenant.getName())
                                            .username(entity.getUsername())
                                            .email(tenant.getRegistEmail())
                                            .mobile(tenant.getPhoneNumber())
                                            .remark(tenant.getDescription())
                                            .status(tenant.getStatusId())
                                            .createDate(tenant.getCreateDate())
                                            .shortName(tenant.getShortName())
                                            .website(tenant.getUrl())
                                            .source(tenant.getSource())
                                            .microsoftId(tenant.getMicrosoftId())
                                            .receiveEmail(tenant.getReceiveEmail())
                                            .registerEmail(tenant.getRegistEmail())
                                            .timezoneId(tenant.getTimezoneFull())
                                            .timezoneName(tenant.getTimezoneShort())
                                            .timezoneOffset(tenant.getTimezoneOffset())
                                            .teamsAuthority(tenant.getTeamsAuthority())
                                            .psa(tenant.getPsa())
                                            .regSource("")
                                            .teamsUserId(entity.getTeamsUserId())
                                            .build();
                                    messagingTemplate.convertAndSend(RabbitConfiguration.EXCHANGE_MSP_TENANT_SYNC, RabbitConfiguration.TENANT_SYNC_INFO, sysTenantDTO);
                                }
                                ,
                                err -> log.error("Create tenant {} administrator user error,msg: {}",
                                        tenant.getId(), err.getMessage()))
                );

    }

    public Mono<Tenant> update(Long id, TenantParam entity) {
        return this.coreClient.put("/tenant/" + id, entity, Tenant.class)
                .doOnSuccess(tenant -> this.usersService.changeUser(UserParam.builder()
                        .username(entity.getUsername())
                        .tenantId(tenant.getId()).email(tenant.getMailPostfix())
                        .password(entity.getPassword()).build()).subscribe()
                );

    }

    public Mono tenantUser(Map<String, Object> params) {
        return this.coreClient.get("tenant/user/search", params)
                .flatMap(resultNode -> {
                    ObjectNode objectNode = resultNode.deepCopy();
                    JsonNode records = objectNode.get("records");
                    List<Mono<TenantUser>> tenantUsers = new ArrayList<>();
                    records.elements().forEachRemaining(entryNode -> {
                        TenantUser tenantUser = objectMapper.convertValue(entryNode, TenantUser.class);
                        Mono<TenantUser> userMono = this.usersService.loadUser(tenantUser.getEmail())
                                .defaultIfEmpty(new User())
                                .map(u -> {
                                    tenantUser.setUsername(u.getUsername());
                                    tenantUser.setAuthorities(u.getAuthorities());
                                    tenantUser.setEnable(u.getEnabled());
                                    tenantUser.setLastModified(u.getModifiedTime());
                                    tenantUser.setLastLogin(u.getLastLogin());
                                    tenantUser.setCreated(u.getCreatedTime());
                                    return tenantUser;
                                });
                        tenantUsers.add(userMono);
                    });
                    return Flux.concat(tenantUsers)
                            .collectList()
                            .map(users -> objectNode.set("records", this.objectMapper.convertValue(users, JsonNode.class)));
                });

    }

    public Flux<TenantUser> tenantUserList(Map<String, Object> params) {
        return this.coreClient.getFlux("tenant/user/list", params, TenantUser.class);
    }

    static class TenantIsExistsException extends RestServerException {

        TenantIsExistsException(int code, String msg) {
            super(code, msg);
        }
    }

    static class UserIsExistsException extends RestServerException {

        UserIsExistsException(String message) {
            this(500, message);
        }

        UserIsExistsException(int position, String message) {
            super(position, message);
        }
    }
}
